종합본 — 안전한 C 코딩 체크리스트와 방어 매크로

NOTE

실습 노트(문자열·동적 메모리·파일 I/O·환경변수·시그널·에러 처리·API 설계·동시성·바이트 순서·컴파일러 경고)에서 반복해서 나온 사고 패턴을 한곳에 모으고, 이를 막는 규칙과 방어 매크로로 연결한 종합본이다. 교육 내용이 추가될 때마다 §2 표와 §5 체크리스트에 행을 더하는 방식으로 갱신한다.

TIP

📂 템플릿 원본 — 템플릿 체화 드릴 저장소 c-playgound의 common/ 이 종합본의 매크로는 모두 아래 헤더를 기준으로 한다. 드릴(D0)에서 이 헤더들을 반복해서 치고, 바뀐 점은 여기 §3에 반영한다.

  • common.h — ret_t 리턴코드, ret_str(), ARRAY_SIZE, UNUSED
  • log.h — LOG_ERR/WRN/INF/DBG, LOG_ERRNO
  • guard.h — CHK_* 가드 매크로
  • safe.h — SAFE_* 정리 매크로
  • mem.h — XNEW, XNEWN, XREALLOC, MUL_OVERFLOW
  • strx.h — str_copy, str_chomp
  • 사용 예: d0_warmup/main.c (take_ptr, take_res)
  • 드릴 스펙: T0. 워밍업 — 규약 백지 쓰기와 빌드 스텁 · 규약 원본: 3. 네이밍·에러·로그·자원해제 규약 · 템플릿 체화 드릴 (블로그)

📌 한 페이지 요약

C에서 사고가 나는 지점은 결국 네 가지다.

  1. 경계 — 버퍼 크기, 널 문자, off-by-one, 정수 오버플로
  2. 수명 — 할당·해제·닫기의 짝, 해제 후 사용, 이중 해제
  3. 실패 처리 — 반환값을 안 보거나, 실패 경로에서 자원을 흘림
  4. 동시성 — 공유 데이터를 서로 다른 락(또는 락 없이)으로 만지거나, 락을 잡는 순서·범위가 어긋남
원칙한 줄 규칙
입력은 믿지 않는다함수 진입부에서 NULL·범위·길이를 먼저 검사한다
쓰기 전에 검사한다크기 검사는 쓰고 나서가 아니라 쓰기 전에
크기 = 글자 수 + 1널 문자 자리를 항상 포함한다
반환값은 전부 본다malloc·fopen·fseek·fclose·rename 모두
자원은 한 곳에서 풀어준다획득 후에는 goto CLEANUP 단일 출구
풀면 NULL로free·fclose 직후 포인터를 NULL로 (SAFE_*)
같은 데이터는 같은 락접근하는 모든 경로가 같은 락을 잡고, 락은 잡은 함수가 푼다
락 순서를 고정한다두 개 이상 잡을 땐 모든 스레드가 같은 순서로 (데드락 방지)
부호 없는 수는 하기 전에 검사뺄셈·증가는 언더플로/랩어라운드 전에 비교

1. 함수 작성의 기본 골격

자원을 잡기 전과 잡은 후에 쓰는 가드가 다르다.

flowchart TD
    S(["함수 진입"]) --> G1["CHK_PTR: 입력 검사<br/>실패 시 즉시 return"]
    G1 --> A1["자원 획득: XNEW · fopen · ..."]
    A1 --> G2{"획득 성공?"}
    G2 -- "아니오" --> C
    G2 -- "예" --> W["작업 수행 · 하위 호출"]
    W --> G3{"실패?"}
    G3 -- "예" --> C
    G3 -- "아니오" --> OK["ret = RET_OK"]
    OK --> C["CLEANUP: 획득의 역순으로 정리<br/>SAFE_FCLOSE_RET · SAFE_FREE"]
    C --> E(["return ret"])
static int do_work(const char *path) {
    int ret = RET_FAIL;
    char *buf = NULL;
    FILE *fp = NULL;
 
    CHK_PTR(path);                       // ① 자원 획득 전: 즉시 return
 
    XNEW(buf);                           // calloc(1, sizeof(*buf)) + 실패 로그
    CHK_ALLOC_GOTO(buf, ret, CLEANUP);   // ② 획득 후: goto로 정리 구간으로 (NO_MEM)
 
    fp = fopen(path, "r");
    CHK_COND_GOTO(fp != NULL, RET_IO_ERROR, ret, CLEANUP);
 
    CHK_RET_GOTO(other_func(), ret, CLEANUP);   // ③ 하위 호출 실패도 전파
    ret = RET_OK;
 
CLEANUP:                                 // ④ 획득의 역순으로 정리
    SAFE_FCLOSE_RET(fp, ret);            // 닫고 NULL로. 쓰기 파일이면 fclose 실패도 ret에 반영
    SAFE_FREE(buf);
    return ret;
}
  • 초기값을 모두 NULL로 잡아 두어야, 어느 단계에서 점프해 와도 SAFE_*가 안전하다.
  • 가드 매크로는 함수가 아니라 텍스트 치환이라 ret을 포인터로 넘기지 않아도 지역변수가 바뀐다. 대신 goto는 다른 함수의 라벨로 못 가므로 함수로 만들 수 없다.

2. 사고 유형 → 원인 → 방어 (핵심 표)


3. 매크로 레퍼런스

3-1. 가드 매크로 (guard.h)

3-2. 정리 매크로 (safe.h)

3-3. 메모리·문자열 보조 헤더

3-4. 매크로 사용 시 함정

3-5. 매크로 vs 함수


4. 매크로로 못 막는 것 — 규칙으로 기억할 것


5. 통합 점검 체크리스트

입력·인자

문자열

메모리

파일

환경변수·시그널

에러 처리·API

동시성·스레드

정수·직렬화·컴파일러 경고

실패 처리


6. 교육 일정별 추가 기록

교육자료 과제와 노트의 대응

노트 링크